#region License

// TweetSharp
// Copyright (c) 2010 Daniel Crenna and Jason Diller
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using Hammock;
using Hammock.Authentication;
using Hammock.Authentication.OAuth;
using Hammock.Caching;
#if SILVERLIGHT
using Hammock.Silverlight.Compat;
#endif
using Hammock.Tasks;
using Hammock.Web;
using TweetSharp.Core;
using TweetSharp.Core.Extensions;
using TweetSharp.Model;

namespace TweetSharp.Fluent
{
    /// <summary>
    /// Abstract base class for a fluent interface.
    /// </summary>
    /// <typeparam name="TResult">The <see cref="TweetSharpResult"/> derived type of result this fluent interface returns</typeparam>
    public abstract class FluentBase<TResult> : IFluentBase<TResult> where TResult : TweetSharpResult
    {
        /// <summary>
        /// Gets or sets the recurring task details
        /// </summary>
        protected ITimedTask<IRateLimitStatus> _recurringTask;

        //todo: encapsulate retry state in its own object
        //retry state
        /// <summary>
        /// gets or sets a value indicating whether or not the is the first attempt at a retryable task
        /// </summary>
        protected bool _firstTry = true;
        /// <summary>
        /// gets or sets the number of remaining retries for a retryable task
        /// </summary>
        protected int _remainingRetries;
        /// <summary>
        /// gets or sets the result of that last attempt at a retryable task
        /// </summary>
        protected TResult _previousResult;
        //end of retry state 

        /// <summary>
        /// Gets or sets the client info associated with queries generated by the fluent interface
        /// </summary>
        protected static IClientInfo _staticClientInfo;

        /// <summary>
        /// Gets or sets the client info associated with the current request
        /// </summary>
        protected static IClientInfo _requestClientInfo;

        /// <summary>
        /// Multithreaded protection for the <see cref="_staticClientInfo"/> field
        /// </summary>
        protected static object _clientInfoLock = new object();

       
        /// <summary>
        /// Creates the result object
        /// </summary>
        /// <param name="response">The <see cref="RestResponseBase"/> object describing the results of the query</param>
        /// <returns>The result of the query</returns>
        protected abstract TResult BuildResult(RestResponseBase response);

        /// <summary>
        /// Gets the URL that was/will be used to perform the current query
        /// </summary>
        /// <param name="ignoreTransparentProxy">if true, the underlying service API url is returned, otherwise the transparent proxy is returned</param>
        /// <returns>The URL for the query</returns>
        public abstract string AsUrl(bool ignoreTransparentProxy);
        
        /// <summary>
        /// Gets the URL that was/will be used to perform the current query
        /// </summary>
        /// <returns>The URL for the query</returns>
        public abstract string AsUrl();
        
        /// <summary>
        /// Gets the OAuth authority url
        /// </summary>
        protected abstract string UrlOAuthAuthority { get; }

#if !SILVERLIGHT
        /// <summary>
        /// Performs the request
        /// </summary>
        /// <returns>The result of the request</returns>
        public abstract TResult Request();
#endif

#if !WindowsPhone
        /// <summary>
        /// Starts the request asynchronously 
        /// </summary>
        /// <returns>The <see cref="IAsyncResult"/> handle for the request</returns>
        public abstract IAsyncResult BeginRequest();

        /// <summary>
        /// Starts the request asynchronously 
        /// </summary>
        /// <returns>The <see cref="IAsyncResult"/> handle for the request</returns>
        public abstract IAsyncResult BeginRequest(object userState);

        /// <summary>
        /// Completes the asynchronous request
        /// </summary>
        /// <param name="asyncResult">The <see cref="IAsyncResult"/>handle returned from <see cref="BeginRequest()"/></param>
        /// <returns></returns>
        public abstract TResult EndRequest(IAsyncResult asyncResult);
#else
        /// <summary>
        /// Starts the request asynchronously.
        /// </summary>
        public abstract void BeginRequest();

        /// <summary>
        /// Starts the request asynchronously.
        /// </summary>
        public abstract void BeginRequest(object userState);
#endif

        /// <summary>
        /// Formulates the query url
        /// </summary>
        /// <returns>the url for this query</returns>
        protected abstract string BuildQuery(bool hasAction, string format, string activity, string action);
        /// <summary>
        /// Gets the internal callback method associated with this query
        /// </summary>
        protected abstract Action<object, TResult> InternalCallback { get; }
       
        /// <summary>
        /// Internal <see cref="RestClient"/> used to execute the query.
        /// </summary>
        protected RestClient Client;
        
        /// <summary>
        /// Constructs a new instance of this class
        /// </summary>
        protected FluentBase()
        {
            Client = new RestClient();
        }

        /// <summary>
        /// Returns the key prepended to the URL when caching queries.
        /// If using basic authentication, this will be the authenticated user's username.
        /// If using OAuth, this will be the authenticated token.
        /// </summary>
        public string CacheKey
        {
            get { return AuthenticationPair != null ? AuthenticationPair.First : string.Empty; }
        }

        /// <summary>
        /// Gets the authentication username and password or key an secret used for the query
        /// </summary>
        public abstract Pair<string, string> AuthenticationPair { get; }

        /// <summary>
        /// Returns the human-readable query to Yammer representing the current expression.
        /// If you are storing URLs for sending later, you can use <code>AsUrl()</code> to return
        /// a URL-encoded string instead.
        /// </summary>
        /// <returns>A URL-decoded string representing this expression's query to Yammer</returns>
        public override string ToString()
        {
            // human-readable; for storing urls, use AsUrl()
            return AsUrl().UrlDecode();
        }

        /// <summary>
        /// Gets or sets the authentication.
        /// </summary>
        /// <value>The authentication.</value>
        public IFluentAuthentication Authentication { get; set; }

        /// <summary>
        /// Gets or sets the method.
        /// </summary>
        /// <value>The method.</value>
        public WebMethod Method { get; set; }

        /// <summary>
        /// Gets or sets the format.
        /// </summary>
        /// <value>The format.</value>
        public WebFormat Format { get; set; }

        /// <summary>
        /// Formulates the OAuth handshake query
        /// </summary>
        /// <returns>the url for the query</returns>
        protected virtual string BuildOAuthQuery()
        {
            var oAuthBase = UrlOAuthAuthority;
            if (!Configuration.TransparentProxy.IsNullOrBlank())
            {
                var authority = Configuration.TransparentProxy;
                oAuthBase = oAuthBase.Replace(UrlOAuthAuthority, authority);
            }

            var oauth = (FluentBaseOAuth) Authentication.Authenticator;
            var url = oAuthBase.FormatWith(oauth.Action);

            if (oauth.Action.Equals("access_token") && !oauth.Verifier.IsNullOrBlank())
            {
                var delim = url.Contains("?") ? '&' : '?';
                url += delim;
                url += "oauth_verifier={0}".FormatWith(oauth.Verifier);
            }

            url = url.Replace("client_auth", "access_token");

            return oauth.Action == "authorize" ? BuildOAuthParameters(oauth, url) : url;
            //return oauth.Action == "authorize" ? url : url;
        }

        /// <summary>
        /// Formulates the oauth parameters
        /// </summary>
        protected virtual string BuildOAuthParameters(IFluentBaseOAuth oauth, string url)
        {
            var parameters = new List<string>(0);
            if (!oauth.Token.IsNullOrBlank())
            {
                parameters.Add("oauth_token={0}".FormatWith(oauth.Token));
            }

            if (!oauth.Callback.IsNullOrBlank())
            {
                parameters.Add("oauth_callback={0}".FormatWith(oauth.Callback));
            }

            var sb = new StringBuilder(url);
            for (var i = 0; i < parameters.Count(); i++)
            {
                sb.Append(i > 0 ? "&" : "?");
                sb.Append(parameters[i]);
            }

            return sb.ToString();
        }

        /// <summary>
        /// Gets or sets the configuration details for the query
        /// </summary>
        public virtual IFluentConfiguration Configuration { get; set; }

        /// <summary>
        /// Sets up the default caching location
        /// </summary>
        protected void EnsureDefaultCache()
        {
            if (Configuration.CacheStrategy == null &&
                (Configuration.CacheAbsoluteExpiration.HasValue ||
                 Configuration.CacheSlidingExpiration.HasValue))
            {
#if !Smartphone && !SILVERLIGHT && !ClientProfiles
                Configuration.CacheStrategy = CacheFactory.AspNetCache;
#else
                Configuration.CacheStrategy = CacheFactory.InMemoryCache;
#endif
            }
        }

        /// <summary>
        /// Gets or sets the amount of time after which the query should be repeated
        /// </summary>
        public TimeSpan RepeatInterval { get; set; }

        /// <summary>
        /// Gets or sets the number of times the query should be repeated
        /// </summary>
        public int RepeatTimes { get; set; }

        /// <summary>
        /// Gets or sets the <see cref="IRateLimitingRule{T}"/> describing how the recurring 
        /// query should be rate limited.
        /// </summary>
        public IRateLimitingRule<IRateLimitStatus> RateLimitingRule { get; set; }
        
        /// <summary>
        /// Cancels the recurring or streaming request, stopping all further occurences.
        /// </summary>
        public void Cancel()
        {
            Client.CancelPeriodicTasks(); 
            Client.CancelStreaming();
        }

        /// <summary>
        /// Gets or sets the recurring task 
        /// </summary>
        public ITimedTask<IRateLimitStatus> RecurringTask
        {
            get { return _recurringTask; }
            set
            {
                if (_recurringTask == null || value == null)
                {
                    _recurringTask = value;
                }
                else
                {
                    throw new InvalidOperationException("Recurring task already set");
                }
            }
        }    

        /// <summary>
        /// Gets a value indicating whether this instance has authentication data present.
        /// </summary>
        /// <value><c>true</c> if this instance has authentication data; otherwise, <c>false</c>.</value>
        public bool HasAuth
        {
            get
            {
                if (Authentication == null)
                {
                    return false;
                }

                var authenticator = Authentication.Authenticator;
                if (authenticator == null)
                {
                    return false;
                }

                if (!(authenticator is FluentBaseBasicAuth))
                {
                    return false;
                }

                return !((FluentBaseBasicAuth) authenticator).Username.IsNullOrBlank() &&
                       !((FluentBaseBasicAuth) authenticator).Password.IsNullOrBlank();
            }
        }

        /// <summary>
        /// Sets the client info.
        /// </summary>
        /// <param name="clientInfo">The client info.</param>
        public static void SetClientInfo(IClientInfo clientInfo)
        {
            lock (_clientInfoLock)
            {
                _staticClientInfo = clientInfo;
            }
        }

        /// <summary>
        /// Gets or sets information about the requesting client
        /// </summary>
        public IClientInfo ClientInfo
        {
            get
            {
                return _requestClientInfo;
            }
            set
            {
                _requestClientInfo = value; 
            }
        }

        /// <summary>
        /// Requests a DELETE resource asynchronously
        /// </summary>
        /// <param name="query">the <see cref="WebQuery"/>to request</param>
        /// <param name="userState">A user provided state object</param>
        protected void RequestDeleteAsync(WebQuery query, object userState)
        {
            var url = AsUrl();
            //don't cache deletes
            query.RequestAsync(url, userState);
        }

        /// <summary>
        /// Abstract method. When overriden in a derived class, performs validation on the update text
        /// </summary>
        //[jd]Changed access to internal from protected as this is accessed from unit tests
        internal abstract void ValidateUpdateText();

        /// <summary>
        /// Gets a value indicating if the query is part of the OAuth setup process
        /// </summary>
        protected bool IsOAuthProcessCall
        {
            get
            {
                if (Authentication.Mode != AuthenticationMode.OAuth)
                {
                    return false;
                }

                if (Authentication == null)
                {
                    return false;
                }

                if (!(Authentication.Authenticator is FluentBaseOAuth))
                {
                    return false;
                }

                var oauth = (FluentBaseOAuth) Authentication.Authenticator;
                return !oauth.Action.Equals("resource");
            }
        }

        /// <summary>
        /// Gets or sets the result of the query
        /// </summary>
        public TResult Result { get; set; }

        /// <summary>
        /// Builds an <see cref="IWebCredentials" /> instance from OAuth data in a query.
        /// </summary>
        /// <returns></returns>
        protected IWebCredentials GetCredentialsFromOAuthAuthenticator()
        {
            IWebCredentials credentials;
            var oauth = (FluentBaseOAuth)Authentication.Authenticator;
            var oauthCredentials = new OAuthCredentials
                                       {
                                           ConsumerKey = oauth.ConsumerKey,
                                           ConsumerSecret = oauth.ConsumerSecret,
                                           ClientUsername = oauth.ClientUsername,
                                           ClientPassword = oauth.ClientPassword,
                                           Token = oauth.Token,
                                           TokenSecret = oauth.TokenSecret,
                                           SignatureMethod = OAuthSignatureMethod.HmacSha1,
                                           ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
                                           CallbackUrl = oauth.Callback,
                                           Verifier = oauth.Verifier
                                       };

            switch (oauth.Action)
            {
                case "resource":
                    oauthCredentials.Type = OAuthType.ProtectedResource;
                    break;
                case "request_token":
                    oauthCredentials.Type = OAuthType.RequestToken;
                    break;
                case "access_token":
                    oauthCredentials.Type = OAuthType.AccessToken;
                    break;
                case "client_auth":
                    oauthCredentials.Type = OAuthType.ClientAuthentication;
                    break;
                case "authorize":
                case "authenticate":
                    oauthCredentials.Type = OAuthType.AccessToken;
                    break;
                default:
                    throw new NotSupportedException("Unknown or unsupported OAuth action");
            }

            credentials = oauthCredentials;
            return credentials;
        }

        /// <summary>
        /// Sets query metadata based on an internally created REST request.
        /// </summary>
        /// <param name="request">The request.</param>
        protected void SetRequestMeta(RestRequest request)
        {
            CreateCachingOptions(request);

            CreateTaskOptions(request);

            request.RetryPolicy = Configuration.RetryPolicy;
            
            if (Configuration.MockGraph != null)
            {
                request.ExpectStatusCode = (HttpStatusCode.OK);
                request.ExpectEntity = Configuration.MockGraph;
            }
            if(Configuration.MockStatusCode.HasValue)
            {
                request.ExpectStatusCode = (HttpStatusCode)Configuration.MockStatusCode.Value;
            }

            var proxy = Configuration.Proxy;
            if (proxy.IsNullOrBlank())
            {
                return;
            }

            if (Uri.IsWellFormedUriString(proxy, UriKind.RelativeOrAbsolute))
            {
                request.Proxy = proxy;
            }
            else
            {
                throw new TweetSharpException(
                    "A proxy '{0}' was specified but was an invalid URI".FormatWith(proxy)
                    );
            }
        }

        private void CreateTaskOptions(RestBase request)
        {
            TaskOptions options; 
            if (RepeatInterval > TimeSpan.Zero)
            {
                if ( RateLimitingRule != null )
                {
                   
                    options = new TaskOptions<IRateLimitStatus>()
                                  {
                                      RateLimitingPredicate = RateLimitingRule.RateLimitIf,
                                      GetRateLimitStatus = RateLimitingRule.GetRateLimitStatus,
                                      RateLimitPercent = RateLimitingRule.LimitToPercentOfTotal,
                                  };
                                   
                }
                else
                {
                    options = new TaskOptions();
                }
                options.RepeatTimes = RepeatTimes;
                options.RepeatInterval = RepeatInterval;
                request.TaskOptions = options;
            }
        }

        private void CreateCachingOptions(RestBase request)
        {
            if (Configuration.CacheStrategy == null)
            {
                return;
            }

            if (Configuration.CacheAbsoluteExpiration.HasValue && 
                Configuration.CacheSlidingExpiration.HasValue)
            {
                throw new ArgumentException("You may only specify one cache expiration on a query");
            }

            var cacheOptions = new CacheOptions();
            if (Configuration.CacheAbsoluteExpiration.HasValue)
            {
                cacheOptions.Mode = CacheMode.AbsoluteExpiration;
                cacheOptions.Duration =
                    Configuration.CacheAbsoluteExpiration.Value.ToUniversalTime() -
                    DateTime.UtcNow;
                        
            }
            else if(Configuration.CacheSlidingExpiration.HasValue)
            {
                cacheOptions.Mode = CacheMode.SlidingExpiration;
                cacheOptions.Duration = Configuration.CacheSlidingExpiration.Value;
            }
            else
            {
                cacheOptions.Mode = CacheMode.NoExpiration;
            }
            request.CacheOptions = cacheOptions;
        }
    }
}